/*
 * Copyright 2009-2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License i distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.paoding.rose.jade.statement;

import net.paoding.rose.jade.annotation.AfterInvocation;
import net.paoding.rose.jade.annotation.ReturnGeneratedKeys;
import net.paoding.rose.jade.annotation.SQLParam;
import net.paoding.rose.jade.annotation.SQLType;
import net.paoding.rose.jade.jpa.JpaDetail;
import net.paoding.rose.jade.statement.interpreter.Interpreter;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Pageable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
 * @author 王志亮 [qieqie.wang@gmail.com]
 */
public class JdbcStatement implements Statement {

    private static final Log sqlLogger = LogFactory.getLog(JdbcStatement.class);

    private final StatementMetaData metaData;

    private final Interpreter interpreter;

    private final Querier querier;

    private final AfterInvocationCallback afterInvocationCallback;

    private final boolean batchUpdate;

    private final SQLType sqlType;

    private final String logPrefix;

    private static final AfterInvocationCallback NULL_AFTER_INVOCATION_CALLBACK = (runtime, returnValue) -> returnValue;

    public JdbcStatement(StatementMetaData statementMetaData, SQLType sqlType, Interpreter interpreter, Querier querier) {
        this.metaData = statementMetaData;
        AfterInvocation afterInvocationAnnotation = metaData.getMethod().getAnnotation(AfterInvocation.class);
        if (afterInvocationAnnotation != null) {
            try {
                this.afterInvocationCallback = afterInvocationAnnotation.value().newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                throw new IllegalArgumentException(e);
            }
        } else {
            this.afterInvocationCallback = NULL_AFTER_INVOCATION_CALLBACK;
        }
        this.interpreter = interpreter;
        this.querier = querier;
        this.sqlType = sqlType;
        if (sqlType == SQLType.WRITE) {
            Method method = statementMetaData.getMethod();
            Class<?>[] types = method.getParameterTypes();
            Class<?> returnType = statementMetaData.getReturnType();
            if (returnType.isPrimitive()) {
                returnType = ClassUtils.primitiveToWrapper(returnType);
            }
            if (types.length == 1 && List.class.isAssignableFrom(types[0])) {
                this.batchUpdate = true;
                if (metaData.getMethod().getAnnotation(ReturnGeneratedKeys.class) != null) {
                    // 批量处理的直接不支持@ReturnGeneratedKeys注解
                    throw new InvalidDataAccessApiUsageException(
                            "batch update method cannot return generated keys: " + method);
                }
                if (returnType != void.class && returnType != int[].class && returnType != Integer.class && returnType != Boolean.class) {
                    throw new InvalidDataAccessApiUsageException("error return type, only support type of {void,boolean,int,int[]}: " + method);
                }
            } else {
                this.batchUpdate = false;
                if (returnType != void.class && returnType != Boolean.class
                        && !Number.class.isAssignableFrom(returnType)) {
                    throw new IllegalArgumentException("error return type:"
                            + method.getDeclaringClass().getName() + "#" + method.getName() + "-->"
                            + returnType);
                }
            }
        } else {
            this.batchUpdate = false;
        }
        this.logPrefix = "\n\t@method:" + this.metaData;
    }

    @Override
    public StatementMetaData getMetaData() {
        return metaData;
    }

    @Override
    public Object execute(Map<String, Object> parameters, Object[] args) {
        Object result;
        if (batchUpdate) {
            result = batchUpdate(parameters);
        } else {
            StatementRuntime runtime = new StatementRuntimeImpl(metaData, parameters);
            if (runtime.isAnalysisWithJap()) {
                Optional<Pageable> pageable = Optional.empty();
                if (Objects.nonNull(args)) {
                    pageable = Arrays.stream(args).filter(o -> o instanceof Pageable).map(o -> (Pageable) o).findFirst();
                }
                JpaDetail jpaDetail = metaData.getJpaUtil().methodToSql(args);
                runtime.setArgs(jpaDetail.getArgs().toArray(new Object[0]));
                runtime.setSQL(jpaDetail.sql(pageable));
            } else {
                this.interpreter.interpret(runtime);
            }
            log(parameters, runtime);
            result = querier.execute(sqlType, batchUpdate, runtime);
            result = afterInvocationCallback.execute(runtime, result);
        }
        if (metaData.getReturnType() == Optional.class && result == null) {
            return Optional.empty();
        }
        return result;

    }

    /**
     * 批量操作
     * @param parameters
     * @return
     */
    private Object batchUpdate(Map<String, Object> parameters) {
        Object result;
        Iterable<?> iterable = (Iterable<?>) parameters.get(":1");
        Iterator<?> iterator = iterable.iterator();
        List<StatementRuntime> runtimes = new LinkedList<>();
        int index = 0;
        while (iterator.hasNext()) {
            Object arg = iterator.next();
            HashMap<String, Object> clone = new HashMap<String, Object>(parameters);
            // 更新执行参数
            clone.put(":1", arg);
            SQLParam sqlParam = metaData.getSQLParamAt(0);
            if (Objects.nonNull(sqlParam)) {
                clone.put(sqlParam.value(), arg);
            } else {
                String paramNameAt = metaData.getMethodParamNameAt(0);
                if (Objects.nonNull(paramNameAt)) clone.put(paramNameAt, arg);
            }
            StatementRuntime runtime = new StatementRuntimeImpl(metaData, clone);
            interpreter.interpret(runtime);
            if (index == 0) log(parameters, runtime);
            runtimes.add(runtime);
            index++;
        }
        result = querier.execute(sqlType, batchUpdate, runtimes.toArray(new StatementRuntime[0]));
        result = afterInvocationCallback.execute(runtimes.get(0), result);
        return result;
    }

    private void log(Map<String, Object> parameters, StatementRuntime runtime) {
        if (sqlLogger.isInfoEnabled()) {
            StringBuilder sql = new StringBuilder(runtime.getSQL());
            List<Object> args = runtime.getArgs();
            StringBuilder sb = new StringBuilder(1024);
            sb.append(logPrefix);
            sb.append("\n\t#SQL:\t").append(runtime.getMetaData().getSQL());
            sb.append("\n\t@sql:\t").append(sql);
            sb.append("\n\t@args:\t");
            ArrayList<String> keys = new ArrayList<String>(parameters.keySet());
            Collections.sort(keys);
            for (String key : keys) {
                sb.append(key).append("='").append(parameters.get(key)).append("'  ");
            }
            sb.append("\n\t$sql:\t").append(runtime.getSQL());
            String[] splitSql = sql.toString().split("\\?");
            if (!ObjectUtils.isEmpty(splitSql) && !CollectionUtils.isEmpty(args) && splitSql.length == args.size()) {
                sql = new StringBuilder();
                for (int i = 0; i < splitSql.length; i++) {
                    sql.append(splitSql[i]).append("'").append(args.get(i)).append("'");
                }
                sb.append("\n\tSQL:\t").append(sql);
            }
            sqlLogger.info(sb.toString());
        }
    }

}
